Skip to content

Harden internal worker boundaries#31

Merged
jdguggs10 merged 2 commits intomainfrom
codex/security-boundary-remediation
Mar 12, 2026
Merged

Harden internal worker boundaries#31
jdguggs10 merged 2 commits intomainfrom
codex/security-boundary-remediation

Conversation

@jdguggs10
Copy link
Owner

@jdguggs10 jdguggs10 commented Mar 12, 2026

Summary

  • split auth-worker into Clerk-only public routes and internal helper routes guarded by X-Flaim-Internal-Token
  • move platform worker auth to real Authorization headers and require internal service auth for worker execution
  • migrate ESPN manual onboarding into Next.js server routes so all platform workers can be binding-only in prod/preview
  • switch auth-worker to Cloudflare native ratelimits config with CI validation and constant-time internal token checks

Testing

  • npm test -- --run src/tests/eval-api-key.test.ts src/tests/logging.test.ts
  • npm run type-check
  • npm test -- --run src/tests/routing.test.ts
  • npm test -- --run src/tests/types.test.ts
  • npm run typecheck
  • npx tsc --noEmit
  • npm run lint
  • npx wrangler deploy --dry-run --env prod
  • npx wrangler deploy --dry-run --env preview

@vercel
Copy link

vercel bot commented Mar 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openai Ready Ready Preview, Comment Mar 12, 2026 0:50am

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security and architecture of internal service communication by introducing a dedicated internal service token. It refactors the auth-worker to clearly delineate public and internal API routes, ensuring that sensitive worker-to-worker interactions are properly authenticated. Furthermore, ESPN onboarding processes have been shifted to the Next.js server, streamlining the deployment and access patterns for platform workers. These changes collectively improve the robustness and maintainability of the system's internal boundaries.

Highlights

  • Internal Service Authentication: Introduced a new INTERNAL_SERVICE_TOKEN for secure, worker-to-worker communication, hardening internal API boundaries across the system.
  • Auth-Worker Refactor: Split auth-worker routes into public (Clerk-only) and internal (requiring INTERNAL_SERVICE_TOKEN in addition to user authentication) endpoints, enhancing security and clarity of access control.
  • ESPN Onboarding Migration: Moved ESPN manual onboarding logic (auto-pull and discover seasons) from the espn-client worker to Next.js server routes, allowing platform workers to be binding-only in production/preview environments.
  • Platform Worker Updates: All platform workers (ESPN, Yahoo, Sleeper) were updated to utilize the new internal service token for secure communication with the auth-worker and other internal services.
  • Cloudflare Rate Limiting: Migrated auth-worker rate limit configurations to Cloudflare native ratelimits in wrangler.jsonc for improved performance and validation, ensuring constant-time internal token checks.
Changelog
  • web/.env.example
    • Added INTERNAL_SERVICE_TOKEN for internal service authentication.
    • Removed NEXT_PUBLIC_ESPN_CLIENT_URL as ESPN onboarding is now handled server-side.
  • web/README.md
    • Updated environment variable documentation to reflect the new internal service token.
    • Removed documentation for NEXT_PUBLIC_ESPN_CLIENT_URL.
  • web/app/api/espn/auto-pull/route.ts
    • Refactored to delegate ESPN auto-pull logic to a new server-side utility function.
    • Removed extensive direct ESPN API interaction logic, simplifying the route handler.
  • web/app/api/espn/discover-seasons/route.ts
    • Refactored to delegate ESPN season discovery logic to a new server-side utility function.
    • Removed direct ESPN API interaction logic, simplifying the route handler.
  • web/lib/server/espn-onboarding.ts
    • Added a new utility file containing the ESPN auto-pull and season discovery logic, centralizing ESPN API interactions and internal auth-worker calls.
    • Implemented functions for fetching ESPN credentials and user leagues from the auth-worker using internal routes.
  • workers/auth-worker/README.md
    • Documented new /internal/ API routes for credentials, leagues, and user preferences.
    • Clarified authentication mechanisms, distinguishing between Clerk-only public routes and internal routes requiring an internal service token.
  • workers/auth-worker/src/tests/eval-api-key.test.ts
    • Updated tests to validate internal API key authentication with the new internal service token and /internal/ routes.
    • Added INTERNAL_SERVICE_TOKEN to the test environment.
  • workers/auth-worker/src/index-hono.ts
    • Implemented internal service token validation for /internal/ routes.
    • Separated public and internal authentication flows using getClerkUserId and getInternalUserId.
    • Restricted raw credential fetching from the public /credentials/espn endpoint, moving it to /internal/credentials/espn/raw.
  • workers/auth-worker/wrangler.jsonc
    • Updated rate limit configurations to use Cloudflare native ratelimits for prod, preview, and dev environments.
    • Removed the deprecated rate_limits top-level configuration.
  • workers/espn-client/README.md
    • Removed documentation for /onboarding/initialize and /onboarding/discover-seasons endpoints.
    • Added a note clarifying the X-Flaim-Internal-Token requirement for the /execute endpoint.
  • workers/espn-client/src/tests/types.test.ts
    • Removed authHeader from ExecuteRequest in test mocks.
  • workers/espn-client/src/index.ts
    • Removed onboarding endpoints (/onboarding/initialize, /onboarding/discover-seasons).
    • Added internal service token validation for the /execute endpoint, requiring X-Flaim-Internal-Token.
  • workers/espn-client/src/onboarding/basic-league-info.ts
    • Removed file, as its functionality was moved to web/lib/server/espn-onboarding.ts.
  • workers/espn-client/src/onboarding/handlers.ts
    • Removed file, as its functionality was moved to web/lib/server/espn-onboarding.ts.
  • workers/espn-client/src/shared/auth.ts
    • Updated getCredentials to fetch raw ESPN credentials from the new internal auth-worker endpoint (/internal/credentials/espn/raw).
  • workers/espn-client/src/types.ts
    • Removed authHeader from the ExecuteRequest interface.
  • workers/espn-client/wrangler.jsonc
    • Disabled workers_dev for production and preview environments, making workers binding-only and not publicly accessible via .workers.dev URLs.
  • workers/fantasy-mcp/src/tests/router.test.ts
    • Updated tests to include INTERNAL_SERVICE_TOKEN in the environment and verify its presence in requests to platform workers.
    • Removed authHeader from the execute request body in tests.
  • workers/fantasy-mcp/src/tests/tools.test.ts
    • Updated tests to use internal auth-worker endpoints (e.g., /internal/leagues) and include the internal service token.
  • workers/fantasy-mcp/src/index.ts
    • Modified the introspection call to auth-worker to use the new internal endpoint (/internal/introspect) and include the internal service token.
  • workers/fantasy-mcp/src/mcp/tools.ts
    • Imported withInternalServiceToken utility.
    • Updated calls to auth-worker for fetching leagues and user preferences to use /internal/ routes and include the internal service token.
  • workers/fantasy-mcp/src/router.ts
    • Imported withInternalServiceToken utility.
    • Modified routeToClient to include the internal service token in requests to platform workers and removed authHeader from the request body.
  • workers/fantasy-mcp/tests/integration/index.integration.test.ts
    • Added INTERNAL_SERVICE_TOKEN_HEADER import and INTERNAL_SERVICE_TOKEN to the buildEnv function.
    • Updated integration tests to reflect the new internal service token and internal auth-worker endpoints.
  • workers/shared/src/auth-fetch.ts
    • Enhanced authWorkerFetch to automatically include the INTERNAL_SERVICE_TOKEN_HEADER for /internal/ paths.
    • Enforced service binding for internal calls in production-like environments, preventing URL fallbacks.
  • workers/shared/src/index.ts
    • Exported new internal service authentication utilities from internal-service.ts.
  • workers/shared/src/internal-service.ts
    • Added a new file defining utilities for internal service authentication, including INTERNAL_SERVICE_TOKEN_HEADER, constantTimeEqual, isProductionLikeEnvironment, requireInternalServiceToken, withInternalServiceToken, and hasValidInternalServiceToken.
  • workers/shared/src/types.ts
    • Added INTERNAL_SERVICE_TOKEN to the BaseEnvWithAuth interface.
  • workers/sleeper-client/README.md
    • Updated documentation to reflect the internal service token requirement for /execute.
    • Removed authHeader from ExecuteRequest documentation.
  • workers/sleeper-client/src/tests/routing.test.ts
    • Updated tests to include INTERNAL_SERVICE_TOKEN in the environment and requests.
    • Added a test case for rejecting requests missing the internal service token.
  • workers/sleeper-client/src/index.ts
    • Added internal service token validation for the /execute endpoint, requiring X-Flaim-Internal-Token.
  • workers/sleeper-client/src/types.ts
    • Removed authHeader from the ExecuteRequest interface.
  • workers/sleeper-client/wrangler.jsonc
    • Disabled workers_dev for production and preview environments.
  • workers/yahoo-client/README.md
    • Updated documentation to reflect the internal service token requirement for /execute.
    • Removed authHeader from ExecuteRequest documentation.
  • workers/yahoo-client/src/index.ts
    • Added internal service token validation for the /execute endpoint, requiring X-Flaim-Internal-Token.
    • Removed authHeader from the ExecuteRequest body processing.
  • workers/yahoo-client/src/shared/auth.ts
    • Updated getYahooCredentials and resolveUserTeamKey to fetch data from internal auth-worker endpoints (/internal/connect/yahoo/credentials, /internal/leagues/yahoo).
  • workers/yahoo-client/src/types.ts
    • Removed authHeader from the ExecuteRequest interface.
  • workers/yahoo-client/wrangler.jsonc
    • Disabled workers_dev for production and preview environments.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/deploy-workers.yml
Activity
  • No specific activity has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@claude
Copy link

claude bot commented Mar 12, 2026

PR Review: Harden internal worker boundaries

Good security-focused refactor. Separating public Clerk-only routes from internal X-Flaim-Internal-Token-gated routes is sound, and the constant-time comparison for the internal token is a nice touch.


BUGS / CORRECTNESS

1. discover-seasons/route.ts sport guard is inconsistent with runEspnDiscoverSeasons

The route still restricts to baseball/football only, but runEspnDiscoverSeasons accepts all four sports via normalizeSport. Basketball/hockey discovery is silently blocked at the route layer while the underlying logic supports it. If intentional, add a comment; if not, remove or align the guard.

2. Duplicate retry block in runEspnDiscoverSeasons (espn-onboarding.ts ~lines 978-1046)

The success/save/error-handling block after the one-second retry is an exact copy (~70 lines) of the first-try block. Extract a helper like processDiscoveredSeason(info, year, ...) to eliminate the duplication and reduce future divergence risk.

3. Fragile 403 vs 401 status determination via string matching

This pattern appears in at least 5 handlers in index-hono.ts. The 403/401 choice is determined by whether authError string contains the header name or a specific phrase. Any wording change silently breaks the status code. Consider adding an isInternalAuthFailure boolean field to AuthResult returned by getInternalUserId, and key off that flag instead.


SECURITY

4. leagueId interpolated into URL path without encoding

In espn-onboarding.ts, patchLeagueTeam builds its URL with leagueId directly interpolated into the path. leagueId comes from user-supplied request body. If it contains / or ? the path is malformed. Use encodeURIComponent(leagueId) here and in the equivalent call in yahoo-client resolveUserTeamKey.

5. ESPN raw credentials now flow through the Next.js server

web/lib/server/espn-onboarding.ts now fetches raw SWID/S2 cookies server-side and forwards them directly to ESPN. This is a broader trust boundary than the old worker-only path. Acceptable given INTERNAL_SERVICE_TOKEN gates the credential fetch, but worth a comment near fetchEspnCredentials documenting the trade-off.


CODE QUALITY / MAINTAINABILITY

6. requireInternalService is copy-pasted across three workers

The same ~20-line helper appears in espn-client/src/index.ts, sleeper-client/src/index.ts, and yahoo-client/src/index.ts. It belongs in workers/shared/src/internal-service.ts as an exported factory or Hono middleware.

7. Rate limit config duplicated 3x in auth-worker/wrangler.jsonc

The ratelimits block is identical across prod, preview, and dev. Wrangler supports a top-level ratelimits key that environments can override, reducing drift risk.

8. workers_dev: false - confirm dev environments still work locally

Disabling .workers.dev is correct for prod isolation. The authWorkerFetch URL fallback now throws in production-like environments (good fail-closed design). Confirm dev environment configs retain workers_dev: true for espn-client, sleeper-client, and yahoo-client so local wrangler dev still functions.


POSITIVE HIGHLIGHTS

  • constantTimeEqual in internal-service.ts is correctly implemented with SHA-256 digests.
  • isProductionLikeEnvironment throwing on a missing binding in prod/preview is correct fail-closed design.
  • Removing authHeader from the JSON body of /execute requests in favor of the HTTP Authorization header is cleaner and more standard.
  • The CI wrangler dry-run validation step with Unexpected fields detection is a useful config regression safeguard.
  • Tests for internal token rejection across auth-worker and platform workers are comprehensive.

MINOR NIT

In auto-pull/route.ts, the console.error log before returning 401 on a missing bearer token was removed. Probably intentional, but worth confirming since it helped debug token issues in production.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 557925a70d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 890 to 891
return c.json({
error: 'unauthorized',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return 403 for failed internal token on Yahoo credential route

This internal-only handler hard-codes 401 on auth failure, so requests missing or with an invalid X-Flaim-Internal-Token are reported as user-auth failures instead of internal boundary failures. Other /internal/* handlers in the same file already map internal-token failures to 403, and keeping this one at 401 makes upstream callers misclassify misconfiguration/forbidden internal access as an end-user re-auth issue.

Useful? React with 👍 / 👎.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is a significant and well-executed pull request that hardens the boundaries between internal services. The introduction of INTERNAL_SERVICE_TOKEN and the separation of public versus internal routes in auth-worker is a great security improvement. Moving the ESPN onboarding logic from a public worker endpoint to a server-side Next.js route handler is also a solid architectural change. The updates across all workers to enforce internal authentication and remove public URLs are consistent and thorough. I have a couple of suggestions to improve logging in the new ESPN onboarding logic.

Comment on lines +554 to +556
} catch {
// Ignore save errors and continue discovery.
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This empty catch block silently swallows errors. While the comment indicates this is intentional to allow discovery to continue, it's best practice to at least log the error. This will make debugging much easier if saving leagues starts failing unexpectedly.

Suggested change
} catch {
// Ignore save errors and continue discovery.
}
} catch (e) {
// Ignore save errors and continue discovery, but log them for debugging.
console.error(`[discover-seasons] Failed to save discovered season ${year} for league ${leagueId}:`, e);
}

Comment on lines +625 to +627
} catch {
// Ignore save errors and continue discovery.
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous comment, this empty catch block should log the error it catches. Silently ignoring errors can hide underlying issues and make troubleshooting difficult.

Suggested change
} catch {
// Ignore save errors and continue discovery.
}
} catch (e) {
// Ignore save errors and continue discovery, but log them for debugging.
console.error(`[discover-seasons] Failed to save discovered season ${year} on retry for league ${leagueId}:`, e);
}

@jdguggs10 jdguggs10 merged commit 1c7a0f6 into main Mar 12, 2026
13 checks passed
@jdguggs10 jdguggs10 deleted the codex/security-boundary-remediation branch March 12, 2026 12:52
@claude
Copy link

claude bot commented Mar 12, 2026

PR Review: Harden internal worker boundaries

This is a solid and well-scoped security PR. The core ideas — moving authHeader out of the request body, splitting public/internal routes, requiring X-Flaim-Internal-Token on all worker-to-worker paths, and disabling workers_dev — are all the right moves. Test coverage is good and the espn-onboarding.ts migration is clean. A few things worth examining before merge:


🔴 Potential Routing Bug — Verify Service Binding Path

workers/fantasy-mcp/src/index.ts and mcp/tools.ts

The service binding URL changed from https://internal/auth/introspecthttps://internal/internal/introspect.

The auth-worker eval tests make requests to /auth/internal/introspect, which implies the api sub-router is mounted at /auth in the root Hono app. If that's the case, the full path to api.get('/internal/introspect') is /auth/internal/introspect, not /internal/introspect. A service binding call to https://internal/internal/introspect would receive a 404.

The integration tests mock AUTH_WORKER, so they don't exercise the auth-worker's internal routing. Please confirm that either:

  • The auth-worker has a separate root-level mount at /internal/... for service binding access, or
  • The service binding path should be https://internal/auth/internal/introspect

If this is wrong, the introspect call will silently fail closed (returns non-OK → 401 to the user).


🟡 CI Validation Assertions Are Silent Failures

.github/workflows/deploy-workers.yml lines 19–20

printf '%s\n' "$output" | grep -q 'env.TOKEN_RATE_LIMITER'
printf '%s\n' "$output" | grep -q 'env.CREDENTIALS_RATE_LIMITER'

Neither line has || exit 1. Without set -e, a failing grep -q just returns a non-zero exit code and the step continues. These assertions are currently no-ops if the pattern is missing.

Fix:

printf '%s\n' "$output" | grep -q 'env.TOKEN_RATE_LIMITER' || { echo "TOKEN_RATE_LIMITER binding missing"; exit 1; }
printf '%s\n' "$output" | grep -q 'env.CREDENTIALS_RATE_LIMITER' || { echo "CREDENTIALS_RATE_LIMITER binding missing"; exit 1; }

🟡 Duplicated requireInternalService Function

espn-client/src/index.ts, sleeper-client/src/index.ts, yahoo-client/src/index.ts

The same ~15-line Hono middleware pattern is copy-pasted verbatim into all three workers. @flaim/worker-shared already provides hasValidInternalServiceToken and INTERNAL_SERVICE_TOKEN_HEADER, but the Hono-specific wrapper isn't shared. If a fourth platform worker is added, this pattern will be duplicated again. Consider a shared createInternalAuthMiddleware helper in the shared library or a note to track this.


🟡 Duplicated "Save Season" Logic in runEspnDiscoverSeasons

web/lib/server/espn-onboarding.ts lines ~926–953 and ~997–1024

The success-path "save discovered season" block (addLeague / 409→patchLeagueTeam / 400 LIMIT_EXCEEDED) is copy-pasted for the retry path. The only difference is info vs retry. This is ~50 lines of duplicated logic. Extracting a saveDiscoveredSeason(authHeader, leagueId, targetSport, year, seasonData, correlationId) helper would make both paths cleaner and prevent divergence.


🟡 Fragile Error-Code Discrimination for 403 vs 401

workers/auth-worker/src/index-hono.ts lines ~1468, ~1607, ~1670, ~1702, ~1734

The HTTP status selection uses string matching:

const status = authError?.includes(INTERNAL_SERVICE_TOKEN_HEADER) 
  || authError?.includes('Internal service authentication') ? 403 : 401;

This pattern appears 4–5 times. If the error message in requireInternalService is ever reworded, the status logic silently breaks. A small structured discriminant (e.g., returning { userId, errorCode: 'MISSING_INTERNAL_TOKEN' | 'INVALID_USER' | ... } from getInternalUserId) would eliminate the string parsing.


🟢 Minor: Content-Type on GET Request

web/lib/server/espn-onboarding.ts line ~562 (getUserLeagues)

headers: buildAuthHeaders(authHeader, correlationId, true),  // true = includeJson

GET /leagues doesn't have a body, so setting Content-Type: application/json is technically incorrect (and could confuse some proxies/CDNs). Pass false or omit the flag here.


🟢 Minor: discover-seasons Route Sport Restriction is Inconsistent

web/app/api/espn/discover-seasons/route.ts line ~14

The route pre-validates sport !== 'baseball' && sport !== 'football', but runEspnDiscoverSeasons in espn-onboarding.ts supports all four sports via normalizeSport. The route-level gate makes discover-seasons silently unavailable for basketball/hockey. If this restriction is intentional, a comment explaining why would help future readers (and the error should say "currently only baseball and football are supported").


✅ What's Good

  • Removing authHeader from request bodies is the right call — auth material should never travel in a JSON body where it could be logged or forwarded.
  • Constant-time comparison (constantTimeEqual via SHA-256) in internal-service.ts is correctly implemented and consistent with the auth-worker's own implementation.
  • workers_dev: false on ESPN, Sleeper, and Yahoo clients in prod/preview correctly removes the public .workers.dev URL surface from platform workers that are now binding-only.
  • Test coverage for the new internal token checks is solid — the "rejects without internal token" tests in routing.test.ts and eval-api-key.test.ts are exactly what's needed.
  • authWorkerFetch throwing in production when the service binding is missing (rather than falling through to a URL) is a good fail-safe.

The routing question is the main thing I'd want confirmed before merging — everything else is safe to defer or fix in follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant